// JAW Edit Version 1.10
// August 19 2007
// Alan To 
// modified by Toby Lawrence (Apr 16 2008)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.IO.Ports;
using System.Xml.Serialization;
using System.Xml;

namespace JAW
{
	public partial class MainForm : Form
	{
		#region Fields

		private DataTable voutProfileTable = new DataTable("voltage out profile");
		private DataTable voutProfileTableHeader = new DataTable();

		#endregion

		public MainForm()
		{
			CheckForIllegalCrossThreadCalls = false;
			InitializeComponent();
		}

		#region UI initialization

		/// <summary>
		/// Initializes the header table.
		/// </summary>
		public void InitializeVoltageOutputTableHeader()
		{
			DataColumn afrColumn = new DataColumn();
			afrColumn.DataType = typeof(string);
			afrColumn.DefaultValue = "AFR";
			voutProfileTableHeader.Columns.Add(afrColumn);

			DataColumn lambdaColumn = new DataColumn();
			lambdaColumn.DataType = typeof(string);
			lambdaColumn.DefaultValue = "Lambda";
			voutProfileTableHeader.Columns.Add(lambdaColumn);

			DataColumn voutColumn = new DataColumn();
			voutColumn.DataType = typeof(string);
			voutColumn.DefaultValue = "Vout1";
			voutProfileTableHeader.Columns.Add(voutColumn);

			DataColumn voutTwoColumn = new DataColumn();
			voutTwoColumn.DataType = typeof(string);
			voutTwoColumn.DefaultValue = "Vout2";
			voutProfileTableHeader.Columns.Add(voutTwoColumn);

			DataRow workRow1 = voutProfileTableHeader.NewRow();
			voutProfileTableHeader.Rows.Add(workRow1);

			// set column width/row height
			voutProfileColumns.DataSource = voutProfileTableHeader;
			voutProfileColumns.Columns[0].Width = 40;
			voutProfileColumns.Columns[1].Width = 50;
			voutProfileColumns.Columns[2].Width = 40;
			voutProfileColumns.Columns[3].Width = 40;
			voutProfileColumns.Rows[0].Height = 14;
			voutProfileColumns.Font = new Font("system", 7);
		}

		/// <summary>
		/// Initializes the voltage output table.
		/// </summary>
		private void InitializeVoltageOutputTable()
		{
			DataColumn afrColumn = new DataColumn();
			afrColumn.DataType = typeof(double);
			voutProfileTable.Columns.Add(afrColumn);

			DataColumn lambdaColumn = new DataColumn();
			lambdaColumn.DataType = typeof(double);
			voutProfileTable.Columns.Add(lambdaColumn);

			DataColumn voutColumn = new DataColumn();
			voutColumn.DataType = typeof(double);
			voutProfileTable.Columns.Add(voutColumn);

			DataColumn voutTwoColumn = new DataColumn();
			voutColumn.DataType = typeof(double);
			voutProfileTable.Columns.Add(voutTwoColumn);

			DataRow workRow;

			// set the initial vout tables to JAWVoltages[], AFR/10
			for (int j = 0; j < 85; j++)
			{
				workRow = voutProfileTable.NewRow();

				workRow[0] = LookupValues.AFR[j];
				workRow[1] = LookupValues.Lambda[j];
				workRow[2] = LookupValues.JAWVoltages[j];
				workRow[3] = LookupValues.JAWVoltages[j];

				voutProfileTable.Rows.Add(workRow);
			}

			ReloadVoltageProfileTable();
		}

		#endregion

		#region Form control callbacks

		/// <summary>
		/// Called when the main form loads.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnMainFormLoaded(object sender, EventArgs e)
		{
			InitializeVoltageOutputTable();
			InitializeVoltageOutputTableHeader();
		}

		#region Device settings

		/// <summary>
		/// Updates the firmware on the device.
		/// </summary>
		/// <remarks>
		/// Firmware update sequence:
		/// step 1 - trigger the bootloader by sending "170" to JAW 
		/// step 2 - send "0" to the bootloader, this will tell the bootloader to send 4 bytes of information; synchronization byte ("128"), device ID, HW version, and bootloader version.
		/// step 3 - ff the synchronization byte is "128" then continue.
		/// step 4 - send "2" to JAW, this tells the bootloader to expect a firmware update.
		/// step 5 - send the first 64 byte block of the firmware, the total firmware size is 7168 bytes so the bootloader will expect 112 64 byte blocks.
		/// step 6 - read back the 64 byte block that was just sent to it for data validation.
		/// step 7 - wait for "255" to be sent from the bootloader, this will indicate that the 64 byte block was finished writing to flash memory.
		/// step 8 - once steps 5, 6, and 7 have repeated 112 times then send "255" to JAW to tell it to exit the bootloader and resume the normal program.
		/// </remarks>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnUpdateFirmwareButtonClicked(object sender, EventArgs e)
		{
			try
			{
				DeviceUpdateError updateError = DeviceUpdateError.None;

				byte[] tx = new byte[Constants.JAWFirmwareSize];
				byte[] rx = new byte[Constants.JAWFirmwareSize];

				DialogResult result = openFirmwareDialog.ShowDialog();
				string firmwareFilename = openFirmwareDialog.FileName.ToString();

				if (result == DialogResult.OK)
				{
					using (FileStream fs = new FileStream(firmwareFilename, FileMode.Open, FileAccess.Read))
					{
						// consider validating the input file here 
						// to avoid flashing an invalid image to the JAW.

						ClosePort();
						OpenPort();

						TriggerDebugMessage(Resources.StartFirmwareUpdate);

						WriteCommandToDevice(JAWCommand.LoadBootloader); // triggers the bootloader.

						System.Threading.Thread.Sleep(50); // need to wait 50 ms for JAW to enter into the bootloader.

						// query device to find synchronization byte, device ID, hardware version, and bootloader version.
						TriggerDebugMessage(Resources.BootloaderTriggered);

						WriteCommandToDevice(JAWCommand.GetDeviceInfo);

						JAWReturnCode deviceSync = ReadResultFromDevice();

						if (deviceSync == JAWReturnCode.Synchronized) // if device is synchronized, continue.
						{
							byte deviceID = ReadDataFromDevice(); // device ID
							byte hwVersion = ReadDataFromDevice(); // HW version
							byte bootloaderVersion = ReadDataFromDevice(); // bootloader version

							// verify these values here, possibly?

							TriggerDebugMessage(Resources.DeviceReady);

							// set device to expect firmware update.
							WriteCommandToDevice(JAWCommand.IncomingFirmwareUpdate);

							int bytesRead = 0;
							if ((bytesRead = fs.Read(tx, 0, Constants.JAWFirmwareSize)) != Constants.JAWFirmwareSize)
							{
								if (bytesRead == 0)
								{
									// we somehow reached the end of the stream. empty file?
									// this is where verification beforehand could be useful.
									// could we close the serial port to reset? or will the 
									// device be waiting indefinitely for the firmware data?
								}
								else if (bytesRead < Constants.JAWFirmwareSize)
								{
									// file is too small, so probably NOT our firmware file
									// again, checking beforehand would solve this most likely.
									// could we close the serial port to reset? or will the 
									// device be waiting indefinitely for the firmware data?
								}
							}

							TriggerDebugMessage(Resources.FirmwareLoaded);

							System.Threading.Thread.Sleep(50);

							for (int i = 0; i < Constants.JAWFirmwareSize; i += Constants.JAWDataChunkSize)
							{
								// write firmware in 64 byte blocks.
								WriteDataToDevice(tx, i, Constants.JAWDataChunkSize);

								// read 64 bytes we sent back from device.
								ReadDataFromDevice(rx, i, Constants.JAWDataChunkSize);

								// verify block was written.
								if (ReadResultFromDevice() == JAWReturnCode.BlockWriteFinished)
								{
									TriggerDebugMessage(Resources.BlockWritten, (i / Constants.JAWDataChunkSize), Constants.JAWFirmwareChunkCount);
								}
							}

							// check to see if the data recieved was the same as sent.
							for (int i = 0; i < Constants.JAWFirmwareSize; i++)
							{
								if (tx[i] != rx[i])
								{
									updateError = DeviceUpdateError.WriteError;

									TriggerDebugMessage(Resources.DataVerificationFailed, i.ToString("X"), i.ToString());

									break;
								}
							}
						}
						else
						{
							// we failed device sync.
							updateError = DeviceUpdateError.DeviceNotSynchronized;
						}

						// exit the bootloader, we've updated the firmware.
						WriteCommandToDevice(JAWCommand.ExitBootloader);

						// close the serial port.
						ClosePort();
					}

					switch (updateError)
					{
						case DeviceUpdateError.None:
							{
								MessageBox.Show(Resources.FirmwareUpdateSuccessful);
								break;
							}
						case DeviceUpdateError.WriteError:
							{
								MessageBox.Show(Resources.WriteError);
								break;
							}
						case DeviceUpdateError.DeviceNotSynchronized:
							{
								MessageBox.Show(Resources.DeviceSyncFailed);
								break;
							}
					}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Gets device information from the device.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnQueryDeviceButtonClicked(object sender, EventArgs e)
		{
			try
			{
				double temp = 0.0d;

				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.LoadBootloader);
				WriteCommandToDevice(JAWCommand.GetDeviceInfo);

				JAWReturnCode deviceSync = ReadResultFromDevice();

				if (deviceSync == JAWReturnCode.Synchronized) // if device is synchronized, continue
				{
					byte deviceID = ReadDataFromDevice(); // device ID
					temp = (double)deviceID / 100 + 1;
					deviceIDValue.Text = temp.ToString();

					byte hardwareID = ReadDataFromDevice(); // hardware ID
					temp = (double)hardwareID / 100 + 1;
					hwVersionValue.Text = temp.ToString();

					byte bootloaderVersion = ReadDataFromDevice(); // bootloader version
					temp = (double)bootloaderVersion / 100 + 1;
					bootloaderVersionValue.Text = temp.ToString();

					WriteCommandToDevice(JAWCommand.ExitBootloader); // tell JAW to exit the bootloader
					WriteCommandToDevice(JAWCommand.GetFirmwareID); // tell JAW to send firmware ID

					byte firmwareID = ReadDataFromDevice(); // firmware ID
					temp = (double)firmwareID / 100 + 1;
					firmwareVersionValue.Text = temp.ToString();

					ClosePort();
				}
				else
				{
					MessageBox.Show(Resources.DeviceSyncFailed);

					WriteCommandToDevice(JAWCommand.ExitBootloader);

					ClosePort();
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		#endregion

		#region Settings

		/// <summary>
		/// Saves voltage profiles and settings to disk.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnSaveSettingsButtonClick(object sender, EventArgs e)
		{
			try
			{
				saveFileDialog.FileName = string.Format("{0}.{1}", Constants.JAWSettingsFilename,
														Constants.JAWSettingsFileExtension);
				saveFileDialog.Filter = string.Format("{0} Files|*.{0}|All Files|*.*", Constants.JAWSettingsFileExtension);

				StringBuilder configLine = new StringBuilder();

				DialogResult result = saveFileDialog.ShowDialog();

				if (result == DialogResult.OK) // user put in a filename
				{
					StreamWriter sw = new StreamWriter(saveFileDialog.FileName.ToString());

					using (XmlTextWriter configWriter = new XmlTextWriter(sw))
					{
						JAWEditSettings settings = new JAWEditSettings();

						settings.ComPort = comPortValueList.SelectedIndex;
						settings.DataLoggingFrequency = dataloggingFrequencyValueList.Text;
						settings.ExternalDisplayOutput = externalDisplayValueList.Text;
						settings.ExternalDisplayRefreshRate = externalDisplayRefreshRateValueList.Text;
						settings.RPMConversionValue = rpmConvValueList.Text;
						settings.VoltageOutputProfileTable = voutProfileTable;

						configWriter.Formatting = Formatting.Indented;
						configWriter.Indentation = 1;
						configWriter.IndentChar = '\t';

						// starts document
						configWriter.WriteStartDocument();

						// starts document root
						configWriter.WriteStartElement("JAWEditSettings");

						configWriter.WriteElementString("ComPort", settings.ComPort.ToString());
						configWriter.WriteElementString("DataLoggingFrequency", settings.DataLoggingFrequency);
						configWriter.WriteElementString("ExternalDisplayOutput", settings.ExternalDisplayOutput);
						configWriter.WriteElementString("ExternalDisplayRefreshRate", settings.ExternalDisplayRefreshRate);
						configWriter.WriteElementString("RPMConversionRate", settings.RPMConversionValue);

						// starts vout profile root
						configWriter.WriteStartElement("VoltageOutProfile");

						for (int i = 0; i < voutProfileTable.Rows.Count; i++)
						{
							configWriter.WriteStartElement("DataPoint");
							configWriter.WriteAttributeString("VoltageOne", voutProfileTable.Rows[i][2].ToString());
							configWriter.WriteAttributeString("VoltageTwo", voutProfileTable.Rows[i][3].ToString());
							configWriter.WriteEndElement();
						}

						// closes vout profile root
						configWriter.WriteEndElement();

						// closes document root
						configWriter.WriteEndElement();

						// closes document
						configWriter.WriteEndDocument();

						//sw.WriteLine(configLine.ToString());

						//configLine.Length = 0; // resets the builder

						//for (int i = 0; i < 85; i++)
						//{
						//    configLine.Append(VoutProfileTable.Rows[i][3].ToString());
						//    configLine.Append(",");
						//}

						//sw.WriteLine(configLine.ToString());

						//sw.WriteLine(comPortValueList.SelectedIndex.ToString());
						//sw.WriteLine(dataloggingFrequencyValueList.Text);
						//sw.WriteLine(externalDisplayValueList.Text);
						//sw.WriteLine(externalDisplayRefreshRateValueList.Text);
						//sw.WriteLine(rpmConvValueList.Text);

						configWriter.Flush();
					}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Loads voltage profiles and settings from disk.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnLoadSettingsButtonClick(object sender, EventArgs e)
		{
			try
			{
				saveFileDialog.FileName = string.Format("{0}.{1}", Constants.JAWSettingsFilename,
														Constants.JAWSettingsFileExtension);
				saveFileDialog.Filter = string.Format("{0} Files|*.{0}|All Files|*.*", Constants.JAWSettingsFileExtension);

				DialogResult result = openFileDialog.ShowDialog();

				if (result == DialogResult.OK)
				{
					FileStream fs = new FileStream(openFileDialog.FileName.ToString(), FileMode.Open, FileAccess.Read);

					using (StreamReader reader = new StreamReader(fs))
					{
						//XmlSerializer deserializer = new XmlSerializer(typeof(JAWEditSettings));

						//JAWEditSettings settings = (JAWEditSettings)deserializer.Deserialize(reader);

						//string settingsHeader = reader.ReadLine();

						//if (settingsHeader != Constants.JAWSettingsFileHeader)
						//{
						//    MessageBox.Show(Resources.ConfigLoadError);
						//    return;
						//}

						//char[] voutDelimter = new char[] { ',' };

						//string vout = reader.ReadLine();
						//string voutTwo = reader.ReadLine();

						//if (vout == null || voutTwo == null)
						//{
						//    MessageBox.Show(Resources.ConfigLoadError);
						//    return;
						//}

						//string[] voutValues = vout.Split(voutDelimter, StringSplitOptions.RemoveEmptyEntries);
						//string[] voutTwoValues = voutTwo.Split(voutDelimter, StringSplitOptions.RemoveEmptyEntries);

						//if (voutValues.Length != 85 || voutTwoValues.Length != 85)
						//{
						//    MessageBox.Show(Resources.ConfigLoadError);
						//    return;
						//}

						//for (int i = 0; i < 85; i++)
						//{
						//    VoutProfileTable.Rows[i][2] = double.Parse(voutValues[i]);
						//}

						//for (int i = 0; i < 85; i++)
						//{
						//    VoutProfileTable.Rows[i][3] = double.Parse(voutTwoValues[i]);
						//}

						//string comPort = reader.ReadLine();
						//string refreshRate = reader.ReadLine();
						//string afrLambdaExternal = reader.ReadLine();
						//string externalDisplayRefresh = reader.ReadLine();
						//string rpmConversion = reader.ReadLine();

						//if (comPort == null || refreshRate == null || afrLambdaExternal == null ||
						//    externalDisplayRefresh == null || rpmConversion == null)
						//{
						//    MessageBox.Show(Resources.ConfigLoadError);
						//    return;
						//}

						XmlDocument settingsDoc = new XmlDocument();
						try
						{
							settingsDoc.Load(reader);
						}
						catch (XmlException)
						{
							MessageBox.Show(Resources.ConfigLoadError);
							return;
						}

						XmlNode rootNode = settingsDoc["JAWEditSettings"];

						if (rootNode == null)
						{
							MessageBox.Show(Resources.ConfigLoadError);
							return;
						}

						comPortValueList.SelectedIndex = int.Parse(rootNode["ComPort"].InnerText);
						dataloggingFrequencyValueList.Text = rootNode["DataLoggingFrequency"].InnerText;
						externalDisplayValueList.Text = rootNode["ExternalDisplayOutput"].InnerText;
						externalDisplayRefreshRateValueList.Text = rootNode["ExternalDisplayRefreshRate"].InnerText;
						rpmConvValueList.Text = rootNode["RPMConversionRate"].InnerText;

						XmlNodeList dataPoints = rootNode.SelectNodes("VoltageOutProfile/DataPoint");

						int rowNumber = 0;

						foreach (XmlNode dataPoint in dataPoints)
						{
							voutProfileTable.Rows[rowNumber][2] = dataPoint.Attributes["VoltageOne"].InnerText;
							voutProfileTable.Rows[rowNumber++][3] = dataPoint.Attributes["VoltageTwo"].InnerText;
						}

						ReloadVoltageProfileTable();
					}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}
		#endregion

		#region Calibration

		/// <summary>
		/// Gets the calibration value from the device.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnGetCalibrationValueButtonClicked(object sender, EventArgs e)
		{
			try
			{
				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.GetCalibrationValue); // tells JAW to send Cal Vale
				sbyte calibrationVal = (sbyte)ReadDataFromDevice(); // reads calibration value

				calibrationValue.Text = calibrationVal.ToString();

				ClosePort();
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Writes a calibration value to the device's EEPROM.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnSaveCalibrationValueButtonClicked(object sender, EventArgs e)
		{
			try
			{
				byte[] tx = new byte[1];

				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.SetCalibrationValue); // tells JAW that the next byte is the calibration value

				sbyte calibrationVal = 0;

				if (sbyte.TryParse(calibrationValue.Text, out calibrationVal))
				{
					tx[0] = (byte)calibrationVal;

					WriteDataToDevice(tx, 0, 1);
				}

				ClosePort();
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Triggers the device to perform a free-air calibration and save the value to it's internal EEPROM.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnCalibrateFreeAirButtonClicked(object sender, EventArgs e)
		{
			MessageBox.Show(Resources.FreeAirCalibrationRequirements);

			try
			{
				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.PerformFreeAirCalibration);

				ClosePort();
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		#endregion

		#region Datalogging

		/// <summary>
		/// Starts/stops datalogging from the device.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnDataloggingButtonClicked(object sender, EventArgs e)
		{
			try
			{
				if (startStopDataloggingButton.Text == "Start Datalogging")
				{
					DataLogging.Instance.ClearData();

					ClosePort();
					OpenPort();

					WriteCommandToDevice(JAWCommand.SendDataloggingInfo); // tell JAW to send 1 packet of data (5 bytes)

					startStopDataloggingButton.Text = "Stop Datalogging";
					ToggleUI(false);

					DataloggingForm view = new DataloggingForm();

					view.ShowDialog();

					dataloggingTimer.Interval = 1000 / int.Parse(dataloggingFrequencyValueList.Text); // starts timer for datalogging
					dataloggingTimer.Start();
				}
				else
				{
					if (devicePort.IsOpen == true)
					{
						dataloggingTimer.Stop();
						ClosePort();

						ToggleUI(true);
					}

					startStopDataloggingButton.Text = "Start Datalogging";
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Save data logging results to disk.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnSaveDatalogButtonClicked(object sender, EventArgs e)
		{
			try
			{
				DateTime now = DateTime.Now;

				saveFileDialog.FileName = string.Format("{0}-{1:dd}-{1:MMM}-{1:yyyy}-{1:HH}{1:mm}{1:ss}.{2}",
														Constants.JAWDatalogFilename, now,
														Constants.JAWDatalogFileExtension);
				saveFileDialog.Filter = "CSV Files|*.CSV|All Files|*.*";


				DialogResult result = saveFileDialog.ShowDialog();

				if (result == DialogResult.OK)
				{
					using (StreamWriter sw = new StreamWriter(saveFileDialog.FileName.ToString()))
					{
						string delimiter = Constants.JAWDataloggingDelimiter;

						string fileHeader = string.Format("{1}{0}{2}{0}{3}{0}{4}{0}{5}{0}{6}",
							delimiter, "AFR", "Lamdba", "RPM", "Voltage_In_1", "Voltage_In_2", "Heat");
						sw.WriteLine(fileHeader);

						foreach (DataEntry entry in DataLogging.Instance)
						{
							sw.WriteLine(entry.ToDelimitedString(delimiter));
						}
					}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Callback for the datalogging update timer.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnDataloggingTimerTick(object sender, EventArgs e)
		{
			try
			{
				double temp = 0.0d;
				int bytesToRead = devicePort.BytesToRead;

				byte[] rx = ReadDataFromDevice(bytesToRead);

				if (bytesToRead == 5)
				{
					if (rx[0] > 84)
					{
						rx[0] = 84;
					}

					double afr = LookupValues.AFR[rx[0]];
					double lambda = LookupValues.Lambda[rx[0]];
					double rpm = rx[2] * int.Parse(rpmConvValueList.Text);

					temp = double.Parse(rx[3].ToString());
					double voltageIn = temp / 51;

					temp = double.Parse(rx[4].ToString());
					double voltageInTwo = temp / 51;

					double sensorTemp = rx[1];

					DataLogging.Instance.StoreEntry(afr, lambda, rpm, voltageIn, voltageInTwo, sensorTemp);
				}

				WriteCommandToDevice(JAWCommand.SendDataloggingInfo);
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		#endregion

		#region Misc. Functions

		/// <summary>
		/// Writes the refresh rate and AFR/Lambda external display option to the device.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnSaveRefreshRateButtonClicked(object sender, EventArgs e)
		{
			try
			{
				byte[] tx = new byte[1];
				double temp = 0.0d;

				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.SetRefreshRate); // tells JAW the the next byte is refresh rate
				temp = double.Parse(externalDisplayRefreshRateValueList.Text);
				tx[0] = (byte)Math.Round((800 / temp), 0);
				WriteDataToDevice(tx, 0, 1);

				WriteCommandToDevice(JAWCommand.SetExternalDisplay); // tells JAW that the next byte is the AFR/Lambda select
				tx[0] = (byte)externalDisplayValueList.SelectedIndex;
				WriteDataToDevice(tx, 0, 1);

				ClosePort();
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Writes the IPR (ignition per revolution) rate to the device to allow RPM datalogging for different ignition system types.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnSaveIPRButtonClicked(object sender, EventArgs e)
		{
			try
			{
				byte[] tx = new byte[1];

				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.SetIPR);

				tx[0] = byte.Parse(iprValueList.Text);
				WriteDataToDevice(tx, 0, 1);

				ClosePort();
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		#endregion

		#region Voltage profiles

		/// <summary>
		/// Uploads analog profiles to the device.
		/// </summary>
		/// <remarks>
		/// Uploading analog profiles sequence:
		/// step 1 - trigger the bootloader by sending "170" to JAW 
		/// step 2 - send "0" to the bootloader, this will tell the bootloader to send 4 bytes of information; synchronization byte ("128"), device ID, HW version, and bootloader version.
		/// step 3 - ff the synchronization byte is "128" then continue.
		/// step 4 - send "1" to JAW, this tells the bootloader to expect a vout update.
		/// step 5 - send the first 64 byte block of the vout tables, the total vout size is 1024 bytes so the bootloader will expect 16 of 64 byte blocks.
		/// step 6 - read back the 64 byte block that was just sent to it for data validation.
		/// step 7 - wait for "255" to be sent from the bootloader, this will indicate that the 64 byte block was finished writing to flash memory.
		/// step 8 - once steps 5, 6, and 7 have repeated 16 times then send "255" to JAW to tell it to exit the bootloader and resume the normal program.
		/// </remarks>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnUpdateVoutPresetButtonClicked(object sender, EventArgs e)
		{
			try
			{
				DeviceUpdateError updateError = DeviceUpdateError.None;

				byte[] tx = new byte[Constants.JAWVoutUpdateSize];
				byte[] rx = new byte[Constants.JAWVoutUpdateSize];

				int writeIndex = 0;
				double temp = 0.0d;

				ClosePort();
				OpenPort();

				WriteCommandToDevice(JAWCommand.LoadBootloader); // triggers the bootloader.

				System.Threading.Thread.Sleep(50); // need to wait 50 ms for JAW to enter into the bootloader.

				// query device to find synchronization byte, device ID, hardware version, and bootloader version.
				TriggerDebugMessage(Resources.BootloaderTriggered);

				WriteCommandToDevice(JAWCommand.GetDeviceInfo);

				JAWReturnCode deviceSync = ReadResultFromDevice();

				if (deviceSync == JAWReturnCode.Synchronized)
				{
					byte deviceID = ReadDataFromDevice(); // device ID
					byte hwVersion = ReadDataFromDevice(); // HW version
					byte bootloaderVersion = ReadDataFromDevice(); // bootloader version

					// verify these values here, possibly?

					// set device to expect vout table update.
					WriteCommandToDevice(JAWCommand.IncomingVoutUpdate);

					for (int i = 0; i < 85; i++)
					{
						temp = double.Parse(voutProfileTable.Rows[i][2].ToString()) * 51;
						temp = Math.Round(temp, 0);
						tx[writeIndex] = (byte)temp;

						writeIndex++;
					}

					for (int i = 0; i < 85; i++)
					{
						temp = double.Parse(voutProfileTable.Rows[i][3].ToString()) * 51;
						temp = Math.Round(temp, 0);
						tx[writeIndex] = (byte)temp;

						writeIndex++;
					}

					// filler data to make 1024 bytes
					for (int i = writeIndex; i < Constants.JAWVoutUpdateSize; i++)
					{
						tx[i] = 0;
					}

					for (int i = 0; i < Constants.JAWVoutUpdateSize; i += Constants.JAWDataChunkSize)
					{
						// write vout data in 64 byte blocks.
						WriteDataToDevice(tx, i, Constants.JAWDataChunkSize);

						// read 64 bytes we sent back from device.
						ReadDataFromDevice(rx, i, Constants.JAWDataChunkSize);

						// verify block was written.
						if (ReadResultFromDevice() == JAWReturnCode.BlockWriteFinished)
						{
							TriggerDebugMessage(Resources.BlockWritten, (i / Constants.JAWDataChunkSize), Constants.JAWVoutUpdateChunkCount);
						}
					}

					// check to see if the data recieved was the same as sent.
					for (int i = 0; i < Constants.JAWVoutUpdateSize; i++)
					{
						if (tx[i] != rx[i])
						{
							updateError = DeviceUpdateError.WriteError;

							TriggerDebugMessage(Resources.DataVerificationFailed, i.ToString("X"), i.ToString());

							break;
						}
					}
				}
				else
				{
					// we failed device sync.
					updateError = DeviceUpdateError.DeviceNotSynchronized;
				}

				// exit the bootloader, we've updated the vout profile.
				WriteCommandToDevice(JAWCommand.ExitBootloader);

				// close the serial port.
				ClosePort();

				switch (updateError)
				{
					case DeviceUpdateError.None:
						{
							MessageBox.Show(Resources.VoutTableUpdateSuccessful);
							break;
						}
					case DeviceUpdateError.WriteError:
						{
							MessageBox.Show(Resources.WriteError);
							break;
						}
					case DeviceUpdateError.DeviceNotSynchronized:
						{
							MessageBox.Show(Resources.DeviceSyncFailed);
							break;
						}
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.ToString());
			}
		}

		/// <summary>
		/// Updates the Vout profile #1 in the AFR/lambda list to the specified profile.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnVoutProfileChanged(object sender, EventArgs e)
		{
			if (voutPresetValueList.SelectedIndex == 0)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][2] = LookupValues.JAWVoltages[i];
				}
			}
			if (voutPresetValueList.SelectedIndex == 1)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][2] = LookupValues.LC1Voltages[i];
				}
			}
			if (voutPresetValueList.SelectedIndex == 2)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][2] = LookupValues.NarrowbandVoltages[i];
				}
			}
		}

		/// <summary>
		/// Updates the Vout profile #2 in the AFR/lambda list to the specified profile.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnVoutProfileTwoChanged(object sender, EventArgs e)
		{
			if (voutPresetTwoValueList.SelectedIndex == 0)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][3] = LookupValues.JAWVoltages[i];
				}
			}
			if (voutPresetTwoValueList.SelectedIndex == 1)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][3] = LookupValues.LC1Voltages[i];
				}
			}
			if (voutPresetTwoValueList.SelectedIndex == 2)
			{
				for (int i = 0; i < 85; i++)
				{
					voutProfileTable.Rows[i][3] = LookupValues.NarrowbandVoltages[i];
				}
			}

		}

		/// <summary>
		/// Interpolates values for the Vout profile #1.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnInterpolateVoutButtonClicked(object sender, EventArgs e)
		{
			double vOutInterped, stoichVolt, voltsPerAFR;

			for (int i = 0; i < 85; i++)
			{
				stoichVolt = double.Parse(stoicAtValue.Text);
				voltsPerAFR = double.Parse(voltsPerAfrValue.Text);

				vOutInterped = Math.Round((stoichVolt - (14.7 - LookupValues.AFR[i]) * voltsPerAFR), 2);

				voutProfileTable.Rows[i][2] = vOutInterped;
			}
		}

		/// <summary>
		/// Interpolates values for the Vout profile #2.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void OnInterpolateVoutTwoButtonClicked(object sender, EventArgs e)
		{
			double vOutInterped, stoichVolt, voltsPerAFR;

			for (int i = 0; i < 85; i++)
			{
				stoichVolt = double.Parse(stoicAtValue.Text);
				voltsPerAFR = double.Parse(voltsPerAfrValue.Text);

				vOutInterped = Math.Round((stoichVolt - (14.7 - LookupValues.AFR[i]) * voltsPerAFR), 2);

				voutProfileTable.Rows[i][3] = vOutInterped;
			}
		}

		#endregion

		#endregion

		#region UI helpers

		/// <summary>
		/// Triggers a pop-up message if debug messages are enabled.
		/// </summary>
		/// <param name="message">the message to show</param>
		private void TriggerDebugMessage(string message)
		{
			if (enableDebugMessagesCheckbox.Checked == true)
			{
				MessageBox.Show(message);
			}
		}

		/// <summary>
		/// Triggers a pop-up message if debug messages are enabled.
		/// </summary>
		/// <param name="message">the message to show</param>
		/// <param name="args">any string format args for the message</param>
		private void TriggerDebugMessage(string message, params object[] args)
		{
			if (enableDebugMessagesCheckbox.Checked == true)
			{
				string updatedMessage = string.Format(message, args);

				MessageBox.Show(updatedMessage);
			}
		}

		/// <summary>
		/// Reloads and reset the voltage profile grid view.
		/// </summary>
		private void ReloadVoltageProfileTable()
		{
			// set coulmn widths
			voutProfileGridView.DataSource = voutProfileTable;
			voutProfileGridView.Columns[0].Width = 40;
			voutProfileGridView.Columns[1].Width = 50;
			voutProfileGridView.Columns[2].Width = 40;
			voutProfileGridView.Columns[3].Width = 40;

			// set row heights
			for (int i = 0; i < voutProfileGridView.RowCount; i++)
			{
				voutProfileGridView.Rows[i].Height = 14;
			}

			// lock afr/lambda columns
			voutProfileGridView.Font = new Font("system", 7);
			voutProfileGridView.Columns[0].ReadOnly = true;
			voutProfileGridView.Columns[1].ReadOnly = true;	
		}

		/// <summary>
		/// Toggles whether or not certain UI elements are enabled.
		/// </summary>
		/// <param name="usable">twhether or not to enable the elements</param>
		private void ToggleUI(bool usable)
		{
			getCalibrationValueButton.Enabled = usable;
			saveCalibrationValueButton.Enabled = usable;
			updateDeviceVoutButton.Enabled = usable;
			updateDeviceFirmwareButton.Enabled = usable;
			calibrateButton.Enabled = usable;
			queryDeviceButton.Enabled = usable;
			dataloggingFrequencyValueList.Enabled = usable;
			comPortValueList.Enabled = usable;
			saveDisplaySettingsButton.Enabled = usable;
			saveIPRButton.Enabled = usable;
			saveDatalogButton.Enabled = usable;
			saveSettingsButton.Enabled = usable;
			loadSettingsButton.Enabled = usable;
			extraButton.Enabled = usable;
		}

		#endregion

		#region Serial port management

		/// <summary>
		/// Writes a command to the device.
		/// </summary>
		/// <param name="command">the command to send</param>
		private void WriteCommandToDevice(JAWCommand command)
		{
			byte[] tx = new byte[1];
			tx[0] = (byte)command;

			try
			{
				devicePort.Write(tx, 0, 1);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}
		}

		/// <summary>
		/// Writes data to the device.
		/// </summary>
		/// <param name="data">the buffer to read from</param>
		/// <param name="offset">the offset to start reading from</param>
		/// <param name="count">the bytes to send</param>
		private void WriteDataToDevice(byte[] data, int offset, int count)
		{
			try
			{
				devicePort.Write(data, offset, count);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}
		}

		/// <summary>
		/// Reads a JAW result code from the device.
		/// </summary>
		/// <returns>a JAW result code (synchronized, write finished, etc)</returns>
		private JAWReturnCode ReadResultFromDevice()
		{
			byte[] tx = new byte[1];

			try
			{
				devicePort.Read(tx, 0, 1);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}

			return (JAWReturnCode)tx[0];
		}

		/// <summary>
		/// Reads a single byte from the device.
		/// </summary>
		/// <returns>a single byte read from the device</returns>
		private byte ReadDataFromDevice()
		{
			byte[] rx = new byte[1];

			try
			{
				devicePort.Read(rx, 0, 1);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}

			return rx[0];
		}

		/// <summary>
		/// Reads a number of bytes from the device.
		/// </summary>
		/// <param name="count">the number of bytes to read</param>
		/// <returns>a byte array with a length of count byte</returns>
		private byte[] ReadDataFromDevice(int count)
		{
			byte[] rx = new byte[count];

			try
			{
				devicePort.Read(rx, 0, count);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}

			return rx;
		}

		/// <summary>
		/// Reads a number of bytes from the device.
		/// </summary>
		/// <param name="buffer">the buffer to read into</param>
		/// <param name="offset">the offset to read into at</param>
		/// <param name="count">the number of bytes to read</param>
		private void ReadDataFromDevice(byte[] buffer, int offset, int count)
		{
			try
			{
				devicePort.Read(buffer, offset, count);
			}
			catch (TimeoutException ex)
			{
				MessageBox.Show(Resources.ReadWriteError);
			}
		}

		/// <summary>
		/// Opens the connection to the device.
		/// </summary>
		private void OpenPort()
		{
			devicePort.PortName = comPortValueList.Text;
			devicePort.Open();
		}

		/// <summary>
		/// Closes the connection to the device.
		/// </summary>
		private void ClosePort()
		{
			if (devicePort.IsOpen)
			{
				devicePort.Close();
			}
		}

		#endregion
	}
}